package webport_web

import (
	"bytes"
	"fmt"
	"io"
	"math/rand"
	"mime/multipart"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"github.com/rs/zerolog/log"
	"wiseverds.com/frame/webport/src/util"
)

func sendResult(c *gin.Context, status int, res interface{}) {
	c.JSON(status, gin.H{
		"result": res,
	})
}

func sendError(c *gin.Context, status int, err interface{}) {
	c.JSON(status, gin.H{
		"error": err,
	})
}

// GET /api/test/ping
func handleTestPing(c *gin.Context) {
	sendResult(c, 200, "pong")
}

// GET /api/test/echo
func handleTestEcho(c *gin.Context) {
	message := c.DefaultQuery("message", "no message")
	sendResult(c, 200, message)
}

// GET /api/test/sleep
func handleTestSleep(c *gin.Context) {
	var params struct {
		Time float64 `form:"time" json:"time" binding:"required"`
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	timeMS := int(params.Time * 1000)
	time.Sleep(time.Duration(timeMS) * time.Millisecond)

	sendResult(c, 200, "success")
}

// GET /api/status/online
func handleStatusOnline(c *gin.Context) {
	sendError(c, 500, "not implemented yet")
}

// Stop this process.
// GET /api/control/stop
func handleControlStop(c *gin.Context) {
	sendError(c, 500, "not implemented yet")
}

// Restart this process.
// GET /api/control/restart
func handleControlRestart(c *gin.Context) {
	sendError(c, 500, "not implemented yet")
}

// Get system type.
// GET /api/system/type
func handleSystemType(c *gin.Context) {
	sendResult(c, 200, runtime.GOOS)
}

// Get system CPU count.
// GET /api/system/cpu_count
func handleSystemCPUCount(c *gin.Context) {
	sendResult(c, 200, runtime.NumCPU())
}

// Get current user name.
// GET /api/system/user
func handleSystemUser(c *gin.Context) {
	user := os.Getenv("USER")
	sendResult(c, 200, user)
}

// Get current user's home directory.
// GET /api/system/user_home
func handleSystemUserHome(c *gin.Context) {
	var home string

	switch runtime.GOOS {
	case "windows":
		home = os.Getenv("USERPROFILE")
	default:
		home = os.Getenv("HOME")
		return
	}

	sendResult(c, 200, home)
}

// Get current system time as unix timestamp.
// GET /api/system/time/unix
func handleSystemTimeUnix(c *gin.Context) {
	time := time.Now().Unix()
	sendResult(c, 200, time)
}

type timeZoneInfo = struct {
	Zone   string `json:"zone"`
	Offset int    `json:"offset"`
}

// Get current system time zone.
// GET /api/system/time/zone
func handleSystemTimeZone(c *gin.Context) {
	zone, offset := time.Now().Zone()

	result := timeZoneInfo{
		Zone:   zone,
		Offset: offset,
	}

	sendResult(c, 200, result)
}

// Get current system local time.
// GET /api/system/time/local
// Query:
//   - format: YYYY-mm-dd HH:MM:SS +TZ
//     Ref: https://pkg.go.dev/time#pkg-constants
func handleSystemTimeLocal(c *gin.Context) {
	format := c.DefaultQuery("format", "2006-01-02 15:04:05 -07:00")
	time := time.Now().Format(format)

	sendResult(c, 200, time)
}

// GET /api/system/env/:name
func handleSystemEnv(c *gin.Context) {
	name := c.Param("name")
	if name == "" {
		sendError(c, 400, "invalid name")
		return
	}

	value := os.Getenv(name)
	sendResult(c, 200, value)
}

func getIPAddress(ipv4, ipv6, loopback bool) (ip_list []string, err error) {
	if ipv4 == false && ipv6 == false {
		return
	}

	ifaces, err := net.Interfaces()
	if err != nil {
		return
	}

	for _, i := range ifaces {
		addrs, err := i.Addrs()
		if err != nil {
			continue
		}

		for _, addr := range addrs {
			var ip net.IP
			switch v := addr.(type) {
			case *net.IPNet:
				ip = v.IP
			case *net.IPAddr:
				ip = v.IP
			}

			// check if it is a loopback address
			if !loopback && ip.IsLoopback() {
				continue
			}

			// check if it is an IPv6 address
			isIPv6 := ip.To4() == nil
			if !ipv6 && isIPv6 {
				continue
			}

			if !ipv4 && !isIPv6 {
				continue
			}

			ip_list = append(ip_list, ip.String())
		}
	}

	return
}

// Get system IP addresses.
// GET /api/system/net/ip
func handleSystemNetIP(c *gin.Context) {
	ip_list, err := getIPAddress(true, false, false)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, ip_list)
}

// Get the current working directory.
// GET /api/fs/cwd
func handleFsCwd(c *gin.Context) {
	cwd, err := os.Getwd()
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, cwd)
}

// Get the current executable path.
// GET /api/fs/exe
func handleFsExe(c *gin.Context) {
	exe, err := os.Executable()
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, exe)
}

type FileInfo struct {
	Name      string `json:"name"`
	Mode      string `json:"mode"`
	Size      int64  `json:"size"`
	IsDir     bool   `json:"isDir"`
	IsRegular bool   `json:"isRegular"`
}

func getFileInfo(path string) (info FileInfo, err error) {
	stat, err := os.Stat(path)
	if err != nil {
		return
	}

	info.Name = stat.Name()
	info.Mode = stat.Mode().String()
	info.Size = stat.Size()
	info.IsDir = stat.IsDir()
	info.IsRegular = stat.Mode().IsRegular()
	return
}

// Get the stat info of a dir entry.
// GET /api/fs/stat
func handleFsStat(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	fileInfo, err := getFileInfo(path)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, fileInfo)
}

// Check if a path exists.
// GET /api/fs/exists
func handleFsExists(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	// check if path exists
	_, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			sendResult(c, 200, false)
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	sendResult(c, 200, true)
}

// Check if the dir entry is a directory.
// GET /api/fs/is_dir
func handleFsIsDir(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	// check if path is a directory
	stat, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			sendResult(c, 200, false)
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	sendResult(c, 200, stat.IsDir())
}

// Check if the dir entry is a regular file.
// GET /api/fs/is_file
func handleFsIsFile(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	// check if path is a directory
	stat, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			sendResult(c, 200, false)
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	isFile := stat.Mode().IsRegular()
	sendResult(c, 200, isFile)
}

// POST /api/fs/chmod
func handleFsChmod(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	modeStr := c.DefaultQuery("mode", "")
	if modeStr == "" {
		sendError(c, 400, "mode is empty")
		return
	}

	mode, err := strconv.ParseUint(modeStr, 8, 32)
	if err != nil {
		sendError(c, 400, err.Error())
		return
	}

	// chmod
	err = os.Chmod(path, os.FileMode(mode))
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

// POST /api/fs/rename
func handleFsRename(c *gin.Context) {
	var params struct {
		From string `form:"from" json:"from" binding:"required"` // old path
		To   string `form:"to" json:"to" binding:"required"`     // new path
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	// rename
	err := os.Rename(params.From, params.To)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

type DirInfo struct {
	Dir     FileInfo   `json:"dir"`
	Entries []FileInfo `json:"entries"`
}

func getDirInfo(path string) (info DirInfo, err error) {
	dirInfo, err := getFileInfo(path)
	if err != nil {
		return
	}

	info.Dir = dirInfo

	files, err := os.ReadDir(path)
	if err != nil {
		return
	}

	for _, file := range files {
		filePath := filepath.Join(path, file.Name())
		fileInfo, err := getFileInfo(filePath)
		if err != nil {
			continue
		}

		info.Entries = append(info.Entries, fileInfo)
	}

	return
}

// GET /api/fs/list_dir
func handleFsListDir(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	dirInfo, err := getDirInfo(path)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, dirInfo)
}

// POST /api/fs/make_dir
func handleFsMakeDir(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	modeStr := c.DefaultQuery("mode", "0755")
	mode, err := strconv.ParseInt(modeStr, 8, 32)
	if err != nil {
		sendError(c, 400, err.Error())
		return
	}

	if mode < 0 || mode > 0777 {
		sendError(c, 400, "invalid mode")
		return
	}

	// make dir
	err = os.Mkdir(path, 0755)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

// POST /api/fs/remove_dir
func handleFsRemoveDir(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	// remove dir
	err := os.Remove(path)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

// GET /api/fs/read_file
func handleFsReadFile(c *gin.Context) {
	path := c.DefaultQuery("path", "")
	if path == "" {
		sendError(c, 400, "invalid path")
		return
	}

	// check if the file exists and readable
	_, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			sendError(c, 404, "file not found")
			return
		} else if os.IsPermission(err) {
			sendError(c, 403, "permission denied")
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	c.File(path)
}

func generateRandomString(alphabet string, length int) string {
	letterRunes := []rune(alphabet)
	buf := make([]rune, length)

	for i := range buf {
		buf[i] = letterRunes[rand.Intn(len(letterRunes))]
	}

	return string(buf)
}

func generateUploadTempFileName(fileName string) string {
	alphabet := "abcdefghijklmnopqrstuvwxyz"
	randString := generateRandomString(alphabet, 8)
	postfix := ".upload." + randString
	tmpFileName := fileName + postfix
	return tmpFileName
}

// Upload a file to the specified path.
// POST /api/fs/write_file
func handleFsWriteFile(c *gin.Context) {
	var params struct {
		Path string                `form:"path" json:"path" binding:"required"`
		Mode string                `form:"mode" json:"mode" binding:"-"`
		File *multipart.FileHeader `form:"file" json:"file" binding:"required"`
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	dirPath := filepath.Dir(params.Path)
	fileName := filepath.Base(params.Path)

	if fileName == "" {
		sendError(c, 400, "invalid file name")
		return
	}

	tmpFileName := generateUploadTempFileName(fileName)
	tmpFilePath := filepath.Join(dirPath, tmpFileName)

	c.SaveUploadedFile(params.File, tmpFilePath)
	os.Rename(tmpFilePath, params.Path)

	// set default mode
	if params.Mode == "" {
		params.Mode = "0644"
	}

	mode, err := strconv.ParseUint(params.Mode, 8, 32)
	if err != nil {
		mode = 0644
	}

	os.Chmod(params.Path, os.FileMode(mode))

	sendResult(c, 200, "success")
}

// POST /api/fs/write_file_chunk
func handleFsWriteFileChunk(c *gin.Context) {
	sendError(c, 500, "not implemented yet")
}

// POST /api/fs/append_file
func handleFsAppendFile(c *gin.Context) {
	sendError(c, 500, "not implemented yet")
}

// POST /api/fs/remove_file
func handleFsRemoveFile(c *gin.Context) {
	var params struct {
		Path string `form:"path" json:"path" binding:"required"`
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	// remove
	err := os.Remove(params.Path)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

// POST /api/shell/open_file
func handleShellOpenFile(c *gin.Context) {
	var params struct {
		Opener string   `form:"opener" json:"opener" binding:"-"`
		Path   string   `form:"path" json:"path" binding:"required"`
		Args   []string `form:"args" json:"args" binding:"-"`
		Cwd    string   `form:"cwd" json:"cwd" binding:"-"`
		Env    []string `form:"env" json:"env" binding:"-"`
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	// check if the file exists and readable
	_, err := os.Stat(params.Path)
	if err != nil {
		if os.IsNotExist(err) {
			sendError(c, 404, "file not found")
			return
		} else if os.IsPermission(err) {
			sendError(c, 403, "permission denied")
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	// set default opener
	if params.Opener == "" {
		switch runtime.GOOS {
		case "windows":
			params.Opener = "explorer"
		case "linux":
			params.Opener = "xdg-open"
		case "darwin":
			params.Opener = "open"
		default:
			sendError(c, 500, "unsupported os")
			return
		}
	}

	args := []string{params.Path}
	args = append(args, params.Args...)

	// open the file
	cmd := exec.Command(params.Opener, args...)

	if params.Cwd != "" {
		cmd.Dir = params.Cwd
	}

	if len(params.Env) > 0 {
		cmd.Env = params.Env
	}

	// start the command
	log.Printf("start command: %v", cmd)
	err = cmd.Start()
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, "success")
}

// POST /api/shell/exec_file
func handleShellExecFile(c *gin.Context) {
	var params struct {
		Path    string   `form:"path" json:"path" binding:"required"` // the file to execute
		Args    []string `form:"args" json:"args" binding:"-"`        // exec args
		Cwd     string   `form:"cwd" json:"cwd" binding:"-"`          // run in this directory
		Env     []string `form:"env" json:"env" binding:"-"`
		Stdin   string   `form:"stdin" json:"stdin" binding:"-"`     // stdin content
		Timeout int32    `form:"timeout" json:"timeout" binding:"-"` // in milliseconds
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	log.Debug().Str("func", "handleShellExecFile").Msgf("params: %v", params)
	for i := 1; i <= len(params.Args); i += 1 {
		log.Debug().Str("func", "handleShellExecFile").Msgf("args[%d]: %v", i, params.Args[i-1])
	}

	log.Debug().Str("func", "handleShellExecFile").Msgf("cwd: %v", params.Cwd)
	log.Debug().Str("func", "handleShellExecFile").Msgf("timeout: %v", params.Timeout)

	// check Content-Type
	ctype := c.Request.Header.Get("Content-Type")
	log.Debug().Str("func", "handleShellExecFile").Msgf("Content-Type: %v", ctype)

	// check if the file exists and readable
	_, err := os.Stat(params.Path)
	if err != nil {
		if os.IsNotExist(err) {
			sendError(c, 404, "file not found")
			return
		} else if os.IsPermission(err) {
			sendError(c, 403, "permission denied")
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	var output bytes.Buffer

	cmd := exec.Command(params.Path, params.Args...)

	if params.Cwd != "" {
		cmd.Dir = params.Cwd
	}

	if len(params.Env) > 0 {
		cmd.Env = params.Env
	}

	cmd.Stdin = nil // params.Stdin
	cmd.Stdout = &output
	cmd.Stderr = &output

	// start the command
	log.Printf("start command: %v", cmd)
	if err := cmd.Start(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Start").Err(err)
		errmsg := fmt.Sprintf("cannot start the command: %v", err)
		sendError(c, 500, errmsg)
		return
	}

	// don't let it run too long
	tmin := 1000 * 5   // 5 seconds
	tmax := 1000 * 120 // 120 seconds
	timeout := util.ClampInt(int(params.Timeout), tmin, tmax)
	duration := time.Duration(timeout) * time.Millisecond

	timer := time.AfterFunc(duration, func() {
		defer func() {
			if err := recover(); err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "timer.AfterFunc").Err(err.(error))
			}
		}()

		cmd.Process.Kill() // 会导致下文 err = "signal: killed"
	})

	// wait the child to exit
	if err := cmd.Wait(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Wait").Err(err)
	}

	// set response Content-Type
	c.Header("Content-Type", "application/octet-stream")

	// put the child's status into response header
	c.Header("X-Process-Status", cmd.ProcessState.String())
	c.Header("X-Process-Exit-Code", util.IntToString(cmd.ProcessState.ExitCode()))

	// stop the timer in any case
	cancelled := timer.Stop() // true: timer尚未触发; false: 已触发
	if cancelled {
		c.Data(200, "application/octet-stream", output.Bytes())
	} else { // timeout
		c.Data(504, "application/octet-stream", output.Bytes())
	}
}

func getDefaultShell() string {
	if runtime.GOOS == "windows" {
		return "cmd"
	} else {
		env_shell := os.Getenv("SHELL")
		if env_shell != "" {
			return env_shell
		} else {
			return "sh"
		}
	}
}

func matchRegexp(str, re string) bool {
	matched, _ := regexp.MatchString(re, str)
	return matched
}

func makeExecCmd(shell, command string, args []string) (cmd *exec.Cmd) {
	shell_name := filepath.Base(shell)
	exec_args := make([]string, 0)

	if matchRegexp(shell_name, `^cmd(\.exe)?$`) {
		exec_args = append(exec_args, "/c")
	} else if matchRegexp(shell_name, `^powershell_name(\.exe)?$`) {
		exec_args = append(exec_args, "-Command")
	} else if matchRegexp(shell_name, `^(a|ba|da|k|z|c|fi)?sh(\.exe)?$`) {
		exec_args = append(exec_args, "-c")
	}

	exec_args = append(exec_args, command)
	exec_args = append(exec_args, args...)

	for i := 1; i <= len(exec_args); i += 1 {
		log.Debug().Str("func", "makeExecCmd").Msgf("exec_args[%d]: %v", i, exec_args[i-1])
	}

	cmd = exec.Command(shell, exec_args...)
	return
}

// POST /api/shell/exec_cmd
func handleShellExecCmd(c *gin.Context) {
	var params struct {
		Shell   string   `form:"shell" json:"shell" binding:"-"`    // the shell to use
		Cmd     string   `form:"cmd" json:"cmd" binding:"required"` // the command to execute
		Args    []string `form:"args" json:"args" binding:"-"`      // exec args
		Cwd     string   `form:"cwd" json:"cwd" binding:"-"`        // run in this directory
		Env     []string `form:"env" json:"env" binding:"-"`
		Stdin   string   `form:"stdin" json:"stdin" binding:"-"`     // stdin content
		Timeout int32    `form:"timeout" json:"timeout" binding:"-"` // in milliseconds
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	log.Debug().Str("func", "handleShellExecCmd").Msgf("params: %v", params)
	for i := 1; i <= len(params.Args); i += 1 {
		log.Debug().Str("func", "handleShellExecCmd").Msgf("args[%d]: %v", i, params.Args[i-1])
	}

	log.Debug().Str("func", "handleShellExecCmd").Msgf("cwd: %v", params.Cwd)
	log.Debug().Str("func", "handleShellExecCmd").Msgf("timeout: %v", params.Timeout)

	// check Content-Type
	ctype := c.Request.Header.Get("Content-Type")
	log.Debug().Str("func", "handleShellExecCmd").Msgf("Content-Type: %v", ctype)

	var shell string
	if params.Shell != "" {
		shell = params.Shell
	} else {
		shell = getDefaultShell()
	}

	cmd := makeExecCmd(shell, params.Cmd, params.Args)

	if params.Cwd != "" {
		cmd.Dir = params.Cwd
	}

	if len(params.Env) > 0 {
		cmd.Env = params.Env
	}

	var output bytes.Buffer

	cmd.Stdin = nil // params.Stdin
	cmd.Stdout = &output
	cmd.Stderr = &output

	// start the command
	log.Printf("start command: %v", cmd)
	if err := cmd.Start(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Start").Err(err)
		errmsg := fmt.Sprintf("cannot start the command: %v", err)
		sendError(c, 500, errmsg)
		return
	}

	// don't let it run too long
	tmin := 1000 * 5   // 5 seconds
	tmax := 1000 * 120 // 120 seconds
	timeout := util.ClampInt(int(params.Timeout), tmin, tmax)
	duration := time.Duration(timeout) * time.Millisecond

	timer := time.AfterFunc(duration, func() {
		defer func() {
			if err := recover(); err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "timer.AfterFunc").Err(err.(error))
			}
		}()

		cmd.Process.Kill() // 会导致下文 err = "signal: killed"
	})

	// wait the child to exit
	if err := cmd.Wait(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Wait").Err(err)
	}

	// set response Content-Type
	c.Header("Content-Type", "application/octet-stream")

	// put the child's status into response header
	c.Header("X-Process-Status", cmd.ProcessState.String())
	c.Header("X-Process-Exit-Code", util.IntToString(cmd.ProcessState.ExitCode()))

	// stop the timer in any case
	cancelled := timer.Stop() // true: timer尚未触发; false: 已触发
	if cancelled {
		c.Data(200, "application/octet-stream", output.Bytes())
	} else { // timeout
		c.Data(504, "application/octet-stream", output.Bytes())
	}
}

// Find a command in PATH.
// GET /api/shell/find_cmd
func handleShellFindCmd(c *gin.Context) {
	var params struct {
		Name string `form:"name" json:"name" binding:"required"`
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	path, err := exec.LookPath(params.Name)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	sendResult(c, 200, path)
}

// GET /api/ws/test/echo
func handleWsTestEcho(c *gin.Context) {
	upgrader := websocket.Upgrader{}
	ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		sendError(c, 500, err.Error())
		return
	}

	defer ws.Close()

	for {
		// read message
		mt, message, err := ws.ReadMessage()
		if err != nil {
			log.Error().Str("func", "handleWsTestEcho").Str("postion", "ws.ReadMessage").Err(err)
			break
		}

		log.Printf("recv: %s", message)

		// write message
		err = ws.WriteMessage(mt, message)
		if err != nil {
			log.Error().Str("func", "handleWsTestEcho").Str("position", "ws.WriteMessage").Err(err)
			break
		}
	}
}

// GET /api/ws/shell/exec_file
func handleWsShellExecFile(c *gin.Context) {
	// RFC6455: the method of a WebSocket request MUST be GET
	method := c.Request.Method
	if method != "GET" {
		sendError(c, 400, "method not allowed")
		return
	}

	var params struct {
		Path    string   `form:"path" json:"path" binding:"required"` // the file to execute
		Args    []string `form:"args" json:"args" binding:"-"`        // exec args
		Cwd     string   `form:"cwd" json:"cwd" binding:"-"`          // run in this directory
		Stdin   string   `form:"stdin" json:"stdin" binding:"-"`      // stdin content
		Timeout int32    `form:"timeout" json:"timeout" binding:"-"`  // in milliseconds
	}

	if err := c.ShouldBind(&params); err != nil {
		sendError(c, 400, err.Error())
		return
	}

	log.Debug().Str("func", "handleShellExecFile").Msgf("params: %v", params)
	for i := 1; i <= len(params.Args); i += 1 {
		log.Debug().Str("func", "handleShellExecFile").Msgf("args[%d]: %v", i, params.Args[i-1])
	}

	log.Debug().Str("func", "handleShellExecFile").Msgf("cwd: %v", params.Cwd)
	log.Debug().Str("func", "handleShellExecFile").Msgf("timeout: %v", params.Timeout)

	// check Content-Type
	ctype := c.Request.Header.Get("Content-Type")
	log.Debug().Str("func", "handleShellExecFile").Msgf("Content-Type: %v", ctype)

	// check if the file exists and readable
	_, err := os.Stat(params.Path)
	if err != nil {
		if os.IsNotExist(err) {
			sendError(c, 404, "file not found")
			return
		} else if os.IsPermission(err) {
			sendError(c, 403, "permission denied")
			return
		} else {
			sendError(c, 500, err.Error())
			return
		}
	}

	// create the command
	cmd := exec.Command(params.Path, params.Args...)
	cmd.Dir = params.Cwd

	// the stdin pipe will be automatically closed
	stdin, err := cmd.StdinPipe()
	if err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.StdinPipe").Err(err)
		errmsg := fmt.Sprintf("cannot create stdin pipe: %v", err)
		sendError(c, 500, errmsg)
		return
	}

	pipe_r, pipe_w := io.Pipe()
	cmd.Stdout = pipe_w
	cmd.Stderr = pipe_w

	defer pipe_r.Close()
	defer pipe_w.Close()

	// start the command
	if err := cmd.Start(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Start").Err(err)
		errmsg := fmt.Sprintf("cannot start the command: %v", err)
		sendError(c, 500, errmsg)
		return
	}

	var isProcessAlive = func() bool {
		return cmd.ProcessState == nil || cmd.ProcessState.ExitCode() == -1
	}

	// upgrade the session to WebSocket
	upgrader := websocket.Upgrader{}
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "upgrader.Upgrade").Err(err)
		return
	}

	defer conn.Close()

	go func() {
		defer func() {
			if isProcessAlive() {
				err := cmd.Process.Kill()
				log.Error().Str("func", "handleShellExecFile").Msgf("kill process %d, result: %v", cmd.Process.Pid, err)
			}
		}()

		for {
			msgtype, p, err := conn.ReadMessage()
			if err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "conn.ReadMessage").Err(err)
				return
			}

			n := len(p)
			log.Trace().Str("func", "handleShellExecFile").Msgf("read %d bytes from client, msgtype=%d", n, msgtype)
			if n <= 0 {
				continue
			}

			if _, err := stdin.Write(p); err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "stdin.Write").Err(err)
				return
			}
		}
	}()

	go func() {
		defer func() {
			if isProcessAlive() {
				err := cmd.Process.Kill()
				log.Error().Str("func", "handleShellExecFile").Msgf("kill process %d, result: %v", cmd.Process.Pid, err)
			}
		}()

		buf := make([]byte, 4096)

		for {
			n, err := pipe_r.Read(buf)
			if err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "pipe_r.Read").Err(err)
				return
			}

			log.Trace().Str("func", "handleShellExecFile").Msgf("read %d bytes from pipe", n)
			if n <= 0 {
				continue
			}

			msgtype := websocket.BinaryMessage
			if err := conn.WriteMessage(msgtype, buf[:n]); err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "conn.WriteMessage").Err(err)
				return
			}
		}
	}()

	// don't let it run too long
	tmin := 1000 * 5    // 5 seconds
	tmax := 1000 * 3600 // 1 hour
	timeout := util.ClampInt(int(params.Timeout), tmin, tmax)
	duration := time.Duration(timeout) * time.Millisecond

	timer := time.AfterFunc(duration, func() {
		defer func() {
			if err := recover(); err != nil {
				log.Error().Str("func", "handleShellExecFile").Str("position", "timer.AfterFunc").Err(err.(error))
			}
		}()

		if isProcessAlive() {
			err := cmd.Process.Kill() // 会导致下文 err = "signal: killed"
			log.Error().Str("func", "handleShellExecFile").Msgf("kill process %d, result: %v", cmd.Process.Pid, err)
		}
	})

	if err := cmd.Wait(); err != nil {
		log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Wait").Err(err)
		return
	}

	// stop the timer in any case
	cancelled := timer.Stop() // true: timer尚未触发; false: 已触发
	if cancelled {
		log.Debug().Str("func", "handleShellExecFile").Msgf("the child process has exited normally")
	} else { // timeout
		log.Debug().Str("func", "handleShellExecFile").Msgf("the child process has been killed by timer")
	}

	/*
		// don't let it run too long
		tmin := 1000 * 5   // 5 seconds
		tmax := 1000 * 120 // 120 seconds
		timeout := util.ClampInt(int(params.Timeout), tmin, tmax)
		duration := time.Duration(timeout) * time.Millisecond

		timer := time.AfterFunc(duration, func() {
			defer func() {
				if err := recover(); err != nil {
					log.Error().Str("func", "handleShellExecFile").Str("position", "timer.AfterFunc").Err(err.(error))
				}
			}()

			cmd.Process.Kill() // 会导致下文 err = "signal: killed"
		})

		// wait the child to exit
		if err := cmd.Wait(); err != nil {
			log.Error().Str("func", "handleShellExecFile").Str("position", "cmd.Wait").Err(err)
		}

		// set response Content-Type
		c.Header("Content-Type", "application/octet-stream")

		// put the child's status into response header
		c.Header("X-Process-Status", cmd.ProcessState.String())
		c.Header("X-Process-Exit-Code", util.IntToString(cmd.ProcessState.ExitCode()))

		// stop the timer in any case
		cancelled := timer.Stop() // true: timer尚未触发; false: 已触发
		if cancelled {
			c.Data(200, "application/octet-stream", output.Bytes())
		} else { // timeout
			c.Data(504, "application/octet-stream", output.Bytes())
		}


		sendResult(c, 500, "not implemented yet")
	*/
}
